﻿global using Devonline.Core;
global using static Devonline.Core.AppSettings;
using System.Reflection;
using InfluxDB.Client;
using InfluxDB.Client.Api.Domain;
using InfluxDB.Client.Core;
using Microsoft.Extensions.Logging;

namespace Devonline.AspNetCore.TimeSeries;

/// <summary>
/// influxdb 数据库 flux 通用操作服务
/// <typeparamref name="TInfluxModel">数据对象模型的类型</typeparamref>
/// </summary>
public class InfluxService<TInfluxModel> : ITimeSeriesDataService<TInfluxModel>, IDisposable where TInfluxModel : class, new()
{
    /// <summary>
    /// 数据库配置终结点
    /// </summary>
    private readonly IDatabaseEndpoint _endpoint;
    /// <summary>
    /// 日志
    /// </summary>
    private readonly ILogger<InfluxService<TInfluxModel>> _logger;
    /// <summary>
    /// InfluxDB Client
    /// </summary>
    private readonly InfluxDBClient _influxClient;

    public InfluxService(IDatabaseEndpoint endpoint, ILogger<InfluxService<TInfluxModel>> logger)
    {
        ArgumentNullException.ThrowIfNull(endpoint.Host);
        ArgumentNullException.ThrowIfNull(endpoint.Token);
        _endpoint = endpoint;
        _logger = logger;

        //InfluxDB 2.0 使用 token 后边不再使用 userName 和 password 模式了
        _influxClient = new InfluxDBClient(endpoint.Host, endpoint.Token);
    }

    /// <summary>
    /// 根据当前查询对象获取 influxdb 查询结果
    /// </summary>
    /// <typeparam name="TInfluxModel"></typeparam>
    /// <param name="request">带时间条件的分页查询请求</param>
    /// <returns></returns>
    public async Task<IEnumerable<TInfluxModel>> QueryAsync(FluxQueryRequest request)
    {
        var flux = GetFluxQueryString(request);
        _logger.LogDebug($"flux query string is: {flux}");
        var queryApi = _influxClient.GetQueryApi();
        return await queryApi.QueryAsync<TInfluxModel>(flux, _endpoint.Domain);
    }

    /// <summary>
    /// 根据当前查询对象获取 influxdb 查询结果
    /// </summary>
    /// <param name="model">待写入的数据</param>
    /// <returns></returns>
    public async Task WriteAsync(TInfluxModel model)
    {
        var retry = 0;
        var success = false;
        while (!success && retry++ <= _endpoint.Retry)
        {
            try
            {
                var api = _influxClient.GetWriteApiAsync();
                _logger.LogDebug("将写入数据到时序库 {host} 中", _endpoint.Host);
                await api.WriteMeasurementAsync(model, GetWritePrecision(_endpoint.Precision), _endpoint.Database, _endpoint.Domain).ConfigureAwait(false);
                _logger.LogInformation("已写入数据到时序库 {host}", _endpoint.Host);
                success = true;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "将数据写入时序数据库 {host} 时出错!", _endpoint.Host);
            }
        }

        if (!success)
        {
            _logger.LogError("将数据写入时序数据库 {host} 时失败, 且经过 {retry} 次重新尝试均不能成功, 本次待写入数据请检查后再做处理!", _endpoint.Host, _endpoint.Retry);
        }
    }
    /// <summary>
    /// 根据当前查询对象获取 influxdb 查询结果
    /// </summary>
    /// <param name="data">待写入的数据</param>
    /// <returns></returns>
    public async Task WritesAsync(IEnumerable<TInfluxModel> data)
    {
        var retry = 0;
        var success = false;
        while (!success && retry++ <= _endpoint.Retry)
        {
            try
            {
                var count = data.Count();
                var api = _influxClient.GetWriteApiAsync();
                _logger.LogDebug("将写入 {count} 条数据到时序数据库 {host} 中", count, _endpoint.Host);
                await api.WriteMeasurementsAsync(data.ToArray(), GetWritePrecision(_endpoint.Precision), _endpoint.Database, _endpoint.Domain).ConfigureAwait(false);
                _logger.LogInformation("已写入 {count} 条数据到时序数据库{host}", count, _endpoint.Host);
                success = true;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "将数据写入时序数据库 {host} 时出错!", _endpoint.Host);
            }
        }

        if (!success)
        {
            _logger.LogError("将数据写入时序数据库 {host} 时失败, 且经过 {retry} 次重新尝试均不能成功, 本次待写入数据请检查后再做处理!", _endpoint.Host, _endpoint.Retry);
        }
    }

    /// <summary>
    /// 释放 InfluxDBClient
    /// </summary>
    public void Dispose()
    {
        _influxClient.Dispose();
        GC.SuppressFinalize(this);
    }

    /// <summary>
    /// 获取 influx flux 查询表达式
    /// </summary>
    /// <param name="request">带时间条件的分页查询请求</param>
    /// <returns></returns>
    private string GetFluxQueryString(FluxQueryRequest request)
    {
        ArgumentNullException.ThrowIfNull(_endpoint.Database);
        ArgumentNullException.ThrowIfNull(_endpoint.DataTable);
        var flux = $"from(bucket:\"{_endpoint.Database}\")" + CHAR_NEW_LINE;

        //范围
        flux += $"|> range(start: {request.Start ?? DateTime.Today.ToString(DEFAULT_ISO_DATETIME_WITH_TIMEZONE_FORMAT)}";
        if (!string.IsNullOrWhiteSpace(request.Stop))
        {
            flux += $", stop: {request.Stop}";
        }

        flux += ")" + CHAR_NEW_LINE;

        //过滤
        flux += $"|> filter(fn: (r) => r._measurement == \"{_endpoint.DataTable ?? typeof(TInfluxModel).GetAttributeValue<Measurement, string>(nameof(Measurement.Name))}\")" + CHAR_NEW_LINE;

        var type = typeof(TInfluxModel);
        var groups = new List<string>();
        var propertyInfos = request.GetType().GetProperties().Where(x => x.CanRead && x.CanWrite);
        if (propertyInfos.Any())
        {
            foreach (var propertyInfo in propertyInfos)
            {
                var column = type.GetProperty(propertyInfo.Name)?.GetCustomAttribute<Column>();
                var value = propertyInfo.GetValue(request)?.ToString();
                if (column != null && !string.IsNullOrWhiteSpace(value))
                {
                    flux += GetFluxFieldQueryString(column.Name, value, column.IsTag);
                    if (column.IsTag)
                    {
                        groups.Add($"\"{column.Name}\"");
                    }
                }
            }
        }

        //flux += "|> toString()" + CHAR_NEW_LINE;

        ////分组
        //if (groups.Count > 0)
        //{
        //    flux += $"|> group(columns: [\"_measurement\", {groups.ToString(", ")}])" + CHAR_NEW_LINE;
        //}

        //排序
        if (string.IsNullOrWhiteSpace(request.OrderBy))
        {
            flux += "|> sort(columns: [\"_time\"], desc: true)" + CHAR_NEW_LINE;
        }
        else
        {
            var orderBy = request.OrderBy;
            var desc = request.OrderBy.EndsWith(" desc");
            if (desc)
            {
                orderBy = orderBy[0..^5];
            }

            var sorts = orderBy.Split(CHAR_COMMA, StringSplitOptions.RemoveEmptyEntries);
            flux += $"|> sort(column: [\"{sorts.ToString(", ")}\"])" + CHAR_NEW_LINE;
        }

        //分页
        if (request.PageIndex < UNIT_ONE)
        {
            request.PageIndex = DEFAULT_PAGE_INDEX;
        }

        if (request.PageSize < UNIT_ONE)
        {
            request.PageSize = DEFAULT_PAGE_SIZE;
        }

        flux += $"|> limit(n: {request.PageSize}, offset: {(request.PageIndex - 1) * request.PageSize})";
        return flux;
    }
    /// <summary>
    /// 单个 influx model field 构造的查询表达式
    /// </summary>
    /// <param name="field"></param>
    /// <param name="value"></param>
    /// <param name="isTag"></param>
    /// <returns></returns>
    private static string GetFluxFieldQueryString(string field, string? value = default, bool isTag = true)
    {
        var result = string.Empty;
        if (!string.IsNullOrWhiteSpace(value))
        {
            var filters = new List<string>();
            var values = value.Split(CHAR_COMMA, StringSplitOptions.RemoveEmptyEntries);
            foreach (var fieldValue in values)
            {
                filters.Add(isTag ? $" r.{field} == \"{fieldValue}\"" : $" r._field == \"{field}\" and r._value == \"{fieldValue}\"");
            }

            if (filters.Count > 0)
            {
                result = $"|> filter(fn: (r) => {filters.ToString(" or ")})" + CHAR_NEW_LINE;
            }
        }

        return result;
    }
    /// <summary>
    /// 获取时间精度
    /// </summary>
    /// <param name="timeKind"></param>
    /// <returns></returns>
    private static WritePrecision GetWritePrecision(TimeKind timeKind) => timeKind switch
    {
        TimeKind.Second => WritePrecision.S,
        TimeKind.MilliSecond => WritePrecision.Ms,
        TimeKind.MicroSecond => WritePrecision.Us,
        _ => WritePrecision.Ns
    };
}
